#pragma once

#include "mosquitto.h"
#include <algorithm>
#include <atomic>
#include <functional>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>

class MqttClient {
private:
    std::string _clientId;
    std::atomic<bool> _isConnected;
    struct mosquitto *_mosquitto;
    std::vector<std::function<void(const std::string &)>> _onConnectCallback;
    std::vector<std::function<void(const std::string &)>> _onDisconnectCallback;
    std::vector<std::function<void(const std::string &, const std::string &, const std::string &)>> _onMessageCallback;
    std::vector<std::function<void(const std::string &, const std::string &, const std::string &)>> _onPublishCallback;
    std::vector<std::function<void(const std::string &, const std::string &)>> _onSubscribeCallback;
    std::vector<std::function<void(const std::string &, const std::string &)>> _onUnSubscribeCallback;

private:
    static void _onConnect(struct mosquitto *m, void *user, int result);
    static void _onDisconnect(struct mosquitto *m, void *user, int result);
    static void _onMessage(struct mosquitto *m, void *user, const mosquitto_message *message);

public:
    explicit MqttClient(const std::string &clientId);
    ~MqttClient() {
        if (_mosquitto) {
            mosquitto_destroy(_mosquitto);
            mosquitto_lib_cleanup();
        }
    }

public:
    template <typename F, typename... Args>
    MqttClient &registerOnConnect(F &&f, Args &&... args) {
        _onConnectCallback.emplace_back(std::bind(std::forward<F>(f), std::forward<Args>(args)..., std::placeholders::_1));
        return *this;
    }

    template <typename F, typename... Args>
    MqttClient &registerOnDisconnect(F &&f, Args &&... args) {
        _onDisconnectCallback.emplace_back(std::bind(std::forward<F>(f), std::forward<Args>(args)..., std::placeholders::_1));
        return *this;
    }

    template <typename F, typename... Args>
    MqttClient &registerOnMessage(F &&f, Args &&... args) {
        _onMessageCallback.emplace_back(std::bind(std::forward<F>(f), std::forward<Args>(args)..., std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
        return *this;
    }

    template <typename F, typename... Args>
    MqttClient &registerOnPublish(F &&f, Args &&... args) {
        _onPublishCallback.emplace_back(std::bind(std::forward<F>(f), std::forward<Args>(args)..., std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
        return *this;
    }

    template <typename F, typename... Args>
    MqttClient &registerOnSubscribe(F &&f, Args &&... args) {
        _onSubscribeCallback.emplace_back(std::bind(std::forward<F>(f), std::forward<Args>(args)..., std::placeholders::_1, std::placeholders::_2));
        return *this;
    }

    template <typename F, typename... Args>
    MqttClient &registerOnUnSubscribe(F &&f, Args &&... args) {
        _onUnSubscribeCallback.emplace_back(std::bind(std::forward<F>(f), std::forward<Args>(args)..., std::placeholders::_1, std::placeholders::_2));
        return *this;
    }

public:
    bool isConnect(void) const { return _isConnected.load(); }
    bool connect(const std::string &host, int port = 1883, int keepAlive = 60) {
        if (_isConnected) {
            return true;
        }

        mosquitto_connect(_mosquitto, host.c_str(), port, keepAlive);
        return !mosquitto_loop_start(_mosquitto);
    }

    bool connect(const std::string &host, const std::string &userName, const std::string &passwd, int port = 1883, int keepAlive = 60) {
        if (_isConnected) {
            return true;
        }
        mosquitto_username_pw_set(_mosquitto, userName.c_str(), passwd.c_str());

        return connect(host, port, keepAlive);
    }

    bool disconnect(void) {
        if (!isConnect()) {
            return true;
        }

        mosquitto_disconnect(_mosquitto);
        return mosquitto_loop_stop(_mosquitto, true);
    }

    bool publish(const std::string &topic, const std::string &payload, int qos = 1) {
        std::for_each(_onPublishCallback.begin(), _onPublishCallback.end(), [this, &topic, &payload](const std::function<void(const std::string &, const std::string &, const std::string &)> &f) { f(_clientId, topic, payload); });
        return !mosquitto_publish(_mosquitto, nullptr, topic.c_str(), payload.length(), payload.c_str(), qos, false);
    }

    bool subscribe(const std::string &topic, int qos = 1) {
        std::for_each(_onSubscribeCallback.begin(), _onSubscribeCallback.end(), [this, &topic](const std::function<void(const std::string &, const std::string &)> &f) { f(_clientId, topic); });
        return !mosquitto_subscribe(_mosquitto, nullptr, topic.c_str(), qos);
    }

    bool unSubscribe(const std::string &topic) {
        std::for_each(_onUnSubscribeCallback.begin(), _onUnSubscribeCallback.end(), [this, &topic](const std::function<void(const std::string &, const std::string &)> &f) { f(_clientId, topic); });
        return !mosquitto_unsubscribe(_mosquitto, nullptr, topic.c_str());
    }
};

inline MqttClient::MqttClient(const std::string &clientId)
    : _clientId(clientId), _mosquitto(nullptr), _isConnected(false) {
    mosquitto_lib_init();

    _mosquitto = mosquitto_new(clientId.c_str(), true, this);
    if (!_mosquitto) {
        throw std::runtime_error("failed to create mosquitto instance");
        return;
    }

    mosquitto_connect_callback_set(_mosquitto, MqttClient::_onConnect);
    mosquitto_disconnect_callback_set(_mosquitto, MqttClient::_onDisconnect);
    mosquitto_message_callback_set(_mosquitto, MqttClient::_onMessage);
}

inline void MqttClient::_onConnect(struct mosquitto *m, void *user, int result) {
    MqttClient *client = static_cast<MqttClient *>(user);
    client->_isConnected = true;
    std::for_each(client->_onConnectCallback.begin(), client->_onConnectCallback.end(), [client](const std::function<void(const std::string &)> &f) { f(client->_clientId); });
}

inline void MqttClient::_onDisconnect(struct mosquitto *m, void *user, int result) {
    MqttClient *client = static_cast<MqttClient *>(user);
    client->_isConnected = false;
    std::for_each(client->_onDisconnectCallback.begin(), client->_onDisconnectCallback.end(), [client](const std::function<void(const std::string &)> &f) { f(client->_clientId); });
}

inline void MqttClient::_onMessage(struct mosquitto *m, void *user, const mosquitto_message *message) {
    MqttClient *client = static_cast<MqttClient *>(user);
    std::for_each(client->_onMessageCallback.begin(), client->_onMessageCallback.end(), [client, message](const std::function<void(const std::string &, const std::string &, const std::string &)> &f) { f(client->_clientId, message->topic, static_cast<char *>(message->payload)); });
}